
require("util")
if not exists("BrickLang_Threads") then BrickLang_Threads = {} end
if not exists("BrickLang_BrickStates") then BrickLang_BrickStates = {} end

local brickMask = ts.get("TypeMasks::FxBrickObjectType")
local getAngleId = memoize(function(brick) return tonumber(ts.callobj(brick, "getAngleId")) end)
local getUiName = memoize(function(brick) return ts.callobj(ts.callobj(brick, "getDatablock"), "getUiName") end)
local getPosition = memoize(function(brick) return vector(ts.callobj(brick, "getPosition")) end)
local function getPrintName(brick) return ts.callobj(brick, "getPrintName") end
local function getColor(brick) return tonumber(ts.callobj(brick, "getColorId")) end
local function getColorFx(brick) return tonumber(ts.callobj(brick, "getColorFxId")) end
local function setColor(brick, color) ts.callobj(brick, "setColor", color) end
local function setColorFx(brick, colorFx) ts.callobj(brick, "setColorFx", colorFx) end
local function isObject(obj) return ts.call("isObject", obj) == "1" end

local dirs = {
	north = vector{0, 1, 0},
	south = vector{0, -1, 0},
	east = vector{1, 0, 0},
	west = vector{-1, 0, 0},
	up = vector{0, 0, 1},
	down = vector{0, 0, -1},
}
local function dirToAngleId(dir)
	if     dir.y >  0.5 then return 0
	elseif dir.y < -0.5 then return 2
	elseif dir.x >  0.5 then return 1
	elseif dir.x < -0.5 then return 3
	elseif dir.z >  0.5 then return 4
	elseif dir.z < -0.5 then return 5
	end
end
local function rotateAngleId(a0, a1) if a0 <= 3 then return (a0 + a1) % 4 else return a0 end end

ts.eval[[ function fxDtsBrick::bl_fireRelay(%brick, %client) { %brick.onRelay(%client); } ]]

local print_funcs = {
	["Letters/-caret"       ] = function(regs) return {dirs.north} end, -- redirect to brick's north direction
	["Letters/-period"      ] = function(regs) regs.acc =            regs.arg; regs.arg = 0; end,
	["Letters/-plus"        ] = function(regs) regs.acc = regs.acc + regs.arg; regs.arg = 0; end,
	["Letters/-minus"       ] = function(regs) regs.acc = regs.acc - regs.arg; regs.arg = 0; end,
	["Letters/-asterisk"    ] = function(regs) regs.acc = regs.acc * regs.arg; regs.arg = 0; end,
	["Letters/-slashright"  ] = function(regs) regs.acc = math.floor(regs.acc / regs.arg); regs.arg = 0; end,
	["Letters/-percent"     ] = function(regs) regs.acc = regs.acc % regs.arg; regs.arg = 0; end,
	["Letters/0"            ] = function(regs) regs.arg = regs.arg*10    ; end,
	["Letters/1"            ] = function(regs) regs.arg = regs.arg*10 + 1; end,
	["Letters/2"            ] = function(regs) regs.arg = regs.arg*10 + 2; end,
	["Letters/3"            ] = function(regs) regs.arg = regs.arg*10 + 3; end,
	["Letters/4"            ] = function(regs) regs.arg = regs.arg*10 + 4; end,
	["Letters/5"            ] = function(regs) regs.arg = regs.arg*10 + 5; end,
	["Letters/6"            ] = function(regs) regs.arg = regs.arg*10 + 6; end,
	["Letters/7"            ] = function(regs) regs.arg = regs.arg*10 + 7; end,
	["Letters/8"            ] = function(regs) regs.arg = regs.arg*10 + 8; end,
	["Letters/9"            ] = function(regs) regs.arg = regs.arg*10 + 9; end,
	["Letters/X"            ] = function(regs) return false end, -- terminate thread
	["Letters/-greater_than"] = function(regs) local arg = regs.arg; regs.arg = 0; if regs.acc > arg then return {dirs.north} elseif regs.acc == arg then return {dirs.east} else return {dirs.south} end; end,
	["Letters/Y"            ] = function(regs) return {dirs.east, dirs.west} end,
	["Letters/O"            ] = function(regs, thread) ts.callobj(thread.brick, "bl_fireRelay", thread.client); end,
	["Letters/T"            ] = function(regs) return { ({dirs.east, dirs.west})[math.random(1, 2)] } end,
	["Letters/M"            ] =
		function(regs, thread)
			local threadDir = dirToAngleId(thread.dir)
			local brickAngleId = getAngleId(thread.brick)
			local dirAngleId = rotateAngleId(threadDir, brickAngleId)
			if dirAngleId==0 or dirAngleId==2 then
				thread.brickStates[thread.brick] = regs.acc
			else
				regs.arg = thread.brickStates[thread.brick] or 0
			end
		end,
	["Letters/C"            ] = function(regs) regs.arg = 0; end,
	["Letters/U"            ] = function(regs) return {dirs.up}; end,
	["Letters/D"            ] = function(regs) return {dirs.down}; end,
	["Letters/N"            ] = function(regs) regs.arg = -regs.arg end,
	["Letters/S"            ] = function(regs) regs.arg, regs.acc = regs.acc, regs.arg end,
}

local function rotateVector(vec, rot)
	rot = rot%4
	local vec2
	if     rot==0 then vec2 = vector{  vec[1],  vec[2], vec[3] }
	elseif rot==1 then vec2 = vector{  vec[2], -vec[1], vec[3] }
	elseif rot==2 then vec2 = vector{ -vec[1], -vec[2], vec[3] }
	elseif rot==3 then vec2 = vector{ -vec[2],  vec[1], vec[3] }
	else error("invalid rotation "..rot) end
	return vec2
end

local function getBrickInDirection(brick, dir)
	local pos1 = getPosition(brick)
	local pos2 = pos1 + dir*50
	local ray = ts.call("containerRaycast", vectostr(pos1), vectostr(pos2), brickMask, brick)
	local hit = ray:match("^[^ ]+")
	if isObject(hit) then
		return tonumber(hit)
	else
		return nil
	end
end

local threadId = 0
local function createThread(threads, brickStates, brick, dir, regs, client)
	local thread = {
		regs = {},
		brick = brick,
		dir = dir,
		id = threadId,
		client = client,
		threads = threads,
		brickStates = brickStates,
	}
	threadId = threadId + 1
	for name, val in pairs(regs) do thread.regs[name] = val end -- copy regs
	table.insert(threads, thread)
	return thread
end
local function advanceThread(thread)
	if isObject(thread.brick) then
		setColorFx(thread.brick, 0)
		local newBrick = getBrickInDirection(thread.brick, thread.dir)
		if newBrick then
			thread.brick = newBrick
			setColorFx(newBrick, 3)
			return true
		else
			return false
		end
	else
		return false
	end
end
local function execThread(thread)
	local brick = thread.brick
	local printName = getPrintName(brick)
	local angleId = getAngleId(brick)
	local func = print_funcs[printName]
	if func then
		local newDirs = func(thread.regs, thread)
		if newDirs then
			for i, d in ipairs(newDirs) do newDirs[i] = rotateVector(d, angleId); end -- rotate all returned dirs by brick's orientation
			if #newDirs>=1 then thread.dir = newDirs[1]; end -- redirect thread if brick calls for it, otherwise keep same dir
			for i = 2, #newDirs do -- make new threads for all other dirs
				createThread(thread.threads, thread.brickStates, brick, newDirs[i], thread.regs, thread.client)
			end
		elseif newDirs==false then -- terminate thread
			return nil
		end
		return true
	else
		return false
	end
end
local function killThread(thread)
	if isObject(thread.brick) then
		setColorFx(thread.brick, 0)
	end
end

local function execAllThreads(threads)
	-- exec all threads and make a list of dead ones
	local toRemove = {}
	for threadIdx, thread in ipairs(threads) do
		if not (advanceThread(thread) and execThread(thread)) then
			table.insert(toRemove, threadIdx)
			killThread(thread)
		end
	end
	-- remove dead threads
	for i = #toRemove, 1, -1 do
		table.remove(threads, toRemove[i])
	end
end
local function killAllThreads(threads)
	for _, thread in ipairs(threads) do
		killThread(thread)
	end
	talk("Killed "..#threads.." threads")
	for i = #threads, 1, -1 do
		threads[i] = nil
	end
end
local function killThreadsByClient(threads, client)
	local toRemove = {}
	for threadIdx, thread in ipairs(threads) do
		if thread.client == client then
			table.insert(toRemove, threadIdx)
			killThread(thread)
		end
	end
	for i = #toRemove, 1, -1 do
		table.remove(threads, toRemove[i])
	end
end

local function createThreadForBrick(threads, brickStates, brick, client)
	local newThread = createThread(threads, brickStates, brick, rotateVector(dirs.north, getAngleId(brick)), {arg = 0, acc = 0}, client)
end

function BrickLang_StartThreadOnBrick(brick, client)
	createThreadForBrick(BrickLang_Threads, BrickLang_BrickStates, tonumber(brick), tonumber(client))
end
function BrickLang_ExecThreads()
	execAllThreads(BrickLang_Threads)
end
function BrickLang_Cancel()
	killAllThreads(BrickLang_Threads)
end
function BrickLang_GetBrickState(brick)
	return BrickLang_BrickStates[tonumber(brick)] or 0
end
function BrickLang_KillThreadsByClient(client)
	killThreadsByClient(BrickLang_Threads, tonumber(client))
end

ts.eval [[
if($BrickLang_TickRate $= "") $BrickLang_TickRate = 33;
function BrickLang_Loop() {
	luacall("BrickLang_ExecThreads");
	
	cancel($BrickLang_sched);
	$BrickLang_sched = schedule($BrickLang_TickRate, 0, BrickLang_Loop);
}
schedule(0, 0, BrickLang_Loop);
function serverCmdBlca(%client) {
	if(%client.isAdmin) luacall("BrickLang_Cancel");
}
function serverCmdBlc(%client) {
	luacall("BrickLang_KillThreadsByClient", %client);
}
function serverCmdBltr(%client, %rate) {
	if(%client.isAdmin) $BrickLang_TickRate = %rate;
}
function fxDtsBrick::spawnThread(%brick, %client) {
	luacall("BrickLang_StartThreadOnBrick", %brick, %client);
}
package BrickLang {
	function fxDTSBrick::onActivate(%brick, %player, %client, %pos, %vec) {
		parent::onActivate(%brick, %player, %client, %pos, %vec);
		if(%brick.getPrintName() $= "Letters/-bang")
			%brick.spawnThread(%client);
		else if(%brick.getPrintName() $= "Letters/M")
			%client.centerPrint("<color:ffffff>" @ luacall("BrickLang_GetBrickState", %brick), 1);
	}
};
if(!$BrickLang_Init) {
	$BrickLang_Init = 1;
	registerOutputEvent("fxDTSBrick", "spawnThread", "", 1);
	activatePackage(BrickLang);
}
function blr() {
	exec("Add-ons/Server_BrickLang/server.cs");
}
]]
